spb/wp-spboucher-ai
Public
1import type { Metadata } from "next";2import Link from "next/link";3import { getLocale } from "@/lib/i18n/server";4import { href, t } from "@/lib/i18n";5import { facets, listPapers } from "@/lib/papers/queries";6import type { ListParams } from "@/lib/papers/types";7import { parseListParams, type SP } from "@/lib/papers/params";8import { PaperCard, PaperRow, PaperTable } from "@/components/paper-items";9import { DisplayToggle, type DisplayMode } from "@/components/display-toggle";10import { Pagination } from "@/components/pagination";11import { Suspense } from "react";12export const dynamic = "force-dynamic";13export async function generateMetadata(): Promise<Metadata> { const d = t(await getLocale()); return { title: d.papers.title, description: d.papers.lead, alternates: { canonical: "/papers" } }; }1415export default async function PapersPage({ searchParams }: { searchParams: Promise<SP> }) {16 const sp = await searchParams; const locale = await getLocale(); const d = t(locale);17 const params = parseListParams(sp); const view = (["list", "compact", "cards", "table"].includes(String(sp.view)) ? sp.view : "list") as DisplayMode;18 const [res, f] = await Promise.all([listPapers(params), facets()]);19 const qs = new URLSearchParams(); for (const [k, v] of Object.entries(sp)) if (v !== undefined && k !== "page") (Array.isArray(v) ? v : [v]).forEach((x) => qs.append(k, x));20 const has = (k: keyof ListParams, v: string) => ((params[k] as string[] | number[] | undefined) ?? []).map(String).includes(v);21 const active = Object.entries({ year: params.year, domain: params.domain, topic: params.topic, author: params.author, keyword: params.keyword, method: params.method, dataset: params.dataset, pub: params.pubStatus, series: params.series, lang: params.lang }).filter(([, v]) => v && v.length).length + (params.pdf ? 1 : 0) + (params.q ? 1 : 0);22 const name = (x: { name_en: string; name_fr: string }) => (locale === "fr" ? x.name_fr : x.name_en);23 const Group = ({ title, k, opts }: { title: string; k: string; opts: { v: string; label: string; n: number }[] }) => opts.length ? (24 <details open={!!(sp[k] && String(sp[k]).length)} className="border-b border-line py-2">25 <summary className="flex justify-between items-center text-sm font-medium py-1">{title}<span className="text-fg-3 text-xs">{opts.length}</span></summary>26 <ul className="mt-1 max-h-56 overflow-auto pr-1 space-y-0.5 text-sm">{opts.map((o) => <li key={o.v}><label className="flex items-center gap-2 py-0.5 cursor-pointer"><input type="checkbox" name={k} value={o.v} defaultChecked={has(k === "pub" ? "pubStatus" : (k as keyof ListParams), o.v)} className="accent-[var(--navy)]" /><span className="flex-1 truncate text-fg-2">{o.label}</span><span className="text-xs text-fg-3 font-mono">{o.n}</span></label></li>)}</ul>27 </details>) : null;28 return (29 <div className="container py-10">30 <header className="mb-6"><h1 className="text-3xl sm:text-4xl">{d.papers.title}</h1><p className="meta mt-2 max-w-3xl">{d.papers.lead}</p></header>31 <div className="grid gap-8 lg:grid-cols-[17rem_1fr]">32 <aside className="lg:sticky lg:top-20 self-start">33 <form method="get" action={href(locale, "/papers")} className="card p-4">34 <input type="hidden" name="view" value={view} /><input type="hidden" name="sort" value={params.sort} />35 <label htmlFor="q" className="label">{d.nav.search}</label>36 <input id="q" name="q" defaultValue={params.q ?? ""} className="input mt-1 mb-3" placeholder={d.search.placeholder} />37 <p className="label mb-1">{d.papers.filters}{active ? ` (${active})` : ""}</p>38 <Group title={d.papers.field} k="domain" opts={f.domains.map((x) => ({ v: x.slug, label: name(x), n: x.n }))} />39 <Group title={d.papers.topic} k="topic" opts={f.topics.map((x) => ({ v: x.slug, label: name(x), n: x.n }))} />40 <Group title={d.papers.year} k="year" opts={f.years.map((x) => ({ v: String(x.year), label: String(x.year), n: x.n }))} />41 <Group title={d.papers.author} k="author" opts={f.authors.map((x) => ({ v: x.slug, label: x.name, n: x.n }))} />42 <Group title={d.papers.method} k="method" opts={f.methods.map((x) => ({ v: x.slug, label: name(x), n: x.n }))} />43 <Group title={d.papers.dataset} k="dataset" opts={f.datasets.map((x) => ({ v: x.slug, label: name(x), n: x.n }))} />44 <Group title={d.papers.keyword} k="keyword" opts={f.keywords.map((x) => ({ v: x.keyword, label: x.keyword, n: x.n }))} />45 <Group title={d.papers.series} k="series" opts={f.series.map((x) => ({ v: x.slug, label: name(x), n: x.n }))} />46 <Group title={d.papers.pubStatus} k="pub" opts={(["working_paper", "under_review", "forthcoming", "published"] as const).map((v) => ({ v, label: d.misc.pubStatus[v], n: 0 })).filter(() => true)} />47 <label className="flex items-center gap-2 text-sm py-2"><input type="checkbox" name="pdf" value="1" defaultChecked={params.pdf} className="accent-[var(--navy)]" />{d.papers.onlyPdf}</label>48 <div className="flex gap-2 mt-2"><button type="submit" className="btn btn-primary btn-sm flex-1">{d.search.go}</button><Link href={href(locale, "/papers")} className="btn btn-sm no-underline">{d.papers.clear}</Link></div>49 </form>50 </aside>51 <section>52 <div className="flex flex-wrap items-center gap-3 mb-4">53 <p className="text-sm text-fg-2"><strong className="text-fg">{res.total}</strong> {res.total === 1 ? d.papers.result : d.papers.results}</p>54 <form method="get" action={href(locale, "/papers")} className="ml-auto flex items-center gap-2 text-sm">55 {[...qs.entries()].filter(([k]) => k !== "sort" && k !== "view").map(([k, v], i) => <input key={i} type="hidden" name={k} value={v} />)}56 <input type="hidden" name="view" value={view} />57 <label htmlFor="sort" className="text-fg-3">{d.papers.sort}</label>58 <select id="sort" name="sort" defaultValue={params.sort} className="input !w-auto !py-1.5 !text-sm">59 {(["recent", "revised", "number", "title", "year", "views", "downloads"] as const).map((s) => <option key={s} value={s}>{d.papers[`sort${s[0].toUpperCase()}${s.slice(1)}` as keyof typeof d.papers] as string}</option>)}60 </select>61 <button type="submit" className="btn btn-sm">OK</button>62 </form>63 <Suspense><DisplayToggle locale={locale} current={view} /></Suspense>64 </div>65 {res.items.length === 0 ? <p className="card p-8 text-center text-fg-2">{d.papers.noResults}</p>66 : view === "cards" ? <div className="grid gap-4 md:grid-cols-2">{res.items.map((p) => <PaperCard key={p.id} p={p} locale={locale} />)}</div>67 : view === "table" ? <PaperTable items={res.items} locale={locale} />68 : <div className="card px-5">{res.items.map((p) => <PaperRow key={p.id} p={p} locale={locale} compact={view === "compact"} />)}</div>}69 <Pagination locale={locale} page={res.page} total={res.total} perPage={res.perPage} base={href(locale, "/papers")} params={qs} />70 {(params.sort === "views" || params.sort === "downloads" || view === "table") && <p className="text-xs text-fg-3 mt-4">{d.papers.popularityNote}</p>}71 </section>72 </div>73 </div>74 );75}76